using System;
using System.Collections.Generic;
using NFreeMarker.Template;
using NFreeMarker.Template.Utility;

namespace NFreeMarker.Core
{
    /**
     * Class to perform arithmetic operations.
     * @author <a href="mailto:jon@revusky.com">Jonathan Revusky</a>
     * @author Attila Szegedi
     */
    public abstract class ArithmeticEngine
    {
        /**
         * Arithmetic engine that converts all numbers to {@link BigDecimal} and
         * then operates on them. This is FreeMarker's default arithmetic engine.
         */
        //public static readonly BigDecimalEngineImpl BigDecimalEngine = new BigDecimalEngineImpl();

        /**
         * Arithmetic engine that uses (more-or-less) the widening conversions of
         * Java language to determine the type of result of operation, instead of
         * converting everything to BigDecimal up front.
         */
        public static readonly ConservativeEngineImpl ConservativeEngine = new ConservativeEngineImpl();

        public abstract int CompareNumbers(Number first, Number second);
        public abstract Number Add(Number first, Number second);
        public abstract Number Subtract(Number first, Number second);
        public abstract Number Multiply(Number first, Number second);
        public abstract Number Divide(Number first, Number second);
        public abstract Number Modulus(Number first, Number second);
        public abstract Number ToNumber(string s);

        //protected int minScale = 12;
        //protected int maxScale = 12;
        //protected int roundingPolicy = BigDecimal.ROUND_HALF_UP;

        ///**
        // * Sets the minimal scale to use when dividing BigDecimal numbers. Default
        // * value is 12.
        // */
        // TODO: public void setMinScale(int minScale) {
        //    if(minScale < 0) {
        //        throw new ArgumentException("minScale < 0");
        //    }
        //    this.minScale = minScale;
        //}

        ///**
        // * Sets the maximal scale to use when multiplying BigDecimal numbers. 
        // * Default value is 100.
        // */
        // TODO: public void setMaxScale(int maxScale) {
        //    if(maxScale < minScale) {
        //        throw new ArgumentException("maxScale < minScale");
        //    }
        //    this.maxScale = maxScale;
        //}

        // TODO: public void setRoundingPolicy(int roundingPolicy) {
        //    if (roundingPolicy != BigDecimal.ROUND_CEILING
        //        && roundingPolicy != BigDecimal.ROUND_DOWN
        //        && roundingPolicy != BigDecimal.ROUND_FLOOR
        //        && roundingPolicy != BigDecimal.ROUND_HALF_DOWN
        //        && roundingPolicy != BigDecimal.ROUND_HALF_EVEN
        //        && roundingPolicy != BigDecimal.ROUND_HALF_UP
        //        && roundingPolicy != BigDecimal.ROUND_UNNECESSARY
        //        && roundingPolicy != BigDecimal.ROUND_UP) 
        //    {
        //        throw new ArgumentException("invalid rounding policy");        
        //    }
            
        //    this.roundingPolicy = roundingPolicy;
        //}

        /**
         * This is the default arithmetic engine in FreeMarker. It converts every
         * number it receives into {@link BigDecimal}, then operates on these
         * converted {@link BigDecimal}s.
         */
        // TODO: public class BigDecimalEngineImpl : ArithmeticEngine
        //{
        //    //public int CompareNumbers(Number first, Number second) {
        //    //    BigDecimal left = toBigDecimal(first);
        //    //    BigDecimal right = toBigDecimal(second);
        //    //    return left.compareTo(right);
        //    //}
        
        //    //public Number Add(Number first, Number second) {
        //    //    BigDecimal left = toBigDecimal(first);
        //    //    BigDecimal right = toBigDecimal(second);
        //    //    return left.add(right);
        //    //}
        
        //    //public Number Subtract(Number first, Number second) {
        //    //    BigDecimal left = toBigDecimal(first);
        //    //    BigDecimal right = toBigDecimal(second);
        //    //    return left.subtract(right);
        //    //}
        
        //    //public Number Multiply(Number first, Number second) {
        //    //    BigDecimal left = toBigDecimal(first);
        //    //    BigDecimal right = toBigDecimal(second);
        //    //    BigDecimal result = left.multiply(right);
        //    //    if (result.scale() > maxScale) {
        //    //        result = result.setScale(maxScale, roundingPolicy);
        //    //    }
        //    //    return result;
        //    //}
        
        //    //public Number Divide(Number first, Number second) {
        //    //    BigDecimal left = toBigDecimal(first);
        //    //    BigDecimal right = toBigDecimal(second);
        //    //    return divide(left, right);
        //    //}
        
        //    //public Number Modulus(Number first, Number second) {
        //    //    long left = first.longValue();
        //    //    long right = second.longValue();
        //    //    return new Long(left % right);
        //    //}

        //    public override Number ToNumber(string s)
        //    {
        //        short shortValue;
        //        if (short.TryParse(s, out shortValue))
        //        {
        //            return new Number.ShortNumber(shortValue);
        //        }
        //        throw new NotImplementedException();
        //    }

        //    //private BigDecimal Divide(BigDecimal left, BigDecimal right) {
        //    //    int scale1 = left.scale();
        //    //    int scale2 = right.scale();
        //    //    int scale = Math.Max(scale1, scale2);
        //    //    scale = Math.Max(minScale, scale);
        //    //    return left.divide(right, scale, roundingPolicy);
        //    //}
        //}

        /**
         * An arithmetic engine that conservatively widens the operation arguments
         * to extent that they can hold the result of the operation. Widening 
         * conversions occur in following situations:
         * <ul>
         * <li>byte and short are always widened to int (alike to Java language).</li>
         * <li>To preserve magnitude: when operands are of different types, the 
         * result type is the type of the wider operand.</li>
         * <li>to avoid overflows: if add, subtract, or multiply would overflow on
         * integer types, the result is widened from int to long, or from long to 
         * BigInteger.</li>
         * <li>to preserve fractional part: if a division of integer types would 
         * have a fractional part, int and long are converted to double, and 
         * BigInteger is converted to BigDecimal. An operation on a float and a 
         * long results in a double. An operation on a float or double and a
         * BigInteger results in a BigDecimal.</li>
         * </ul>
         */
        public class ConservativeEngineImpl : ArithmeticEngine
        {
            private const int Integer = 0;
            private const int Long = 1;
            private const int Float = 2;
            private const int Double = 3;
            // TODO: private const int BIGINTEGER = 4;
            // TODO: private const int BIGDECIMAL = 5;

            private static readonly Dictionary<Type, int> ClassCodes = CreateClassCodesMap();

            public override int CompareNumbers(Number first, Number second)
            {
                switch (GetCommonClassCode(first, second))
                {
                    case Integer:
                        {
                            int n1 = first.IntValue();
                            int n2 = second.IntValue();
                            return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1);
                        }
                    case Long:
                        {
                            long n1 = first.LongValue();
                            long n2 = second.LongValue();
                            return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1);
                        }
                    case Float:
                        {
                            float n1 = first.FloatValue();
                            float n2 = second.FloatValue();
                            return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1);
                        }
                    case Double:
                        {
                            double n1 = first.DoubleValue();
                            double n2 = second.DoubleValue();
                            return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1);
                        }
                    //case BIGINTEGER:
                    //    {
                    //        BigInteger n1 = toBigInteger(first);
                    //        BigInteger n2 = toBigInteger(second);
                    //        return n1.compareTo(n2);
                    //    }
                    //case BIGDECIMAL:
                    //    {
                    //        BigDecimal n1 = toBigDecimal(first);
                    //        BigDecimal n2 = toBigDecimal(second);
                    //        return n1.compareTo(n2);
                    //    }
                }
                // Make the compiler happy. getCommonClassCode() is guaranteed to 
                // return only above codes, or throw an exception.
                throw new InvalidOperationException();
            }

            public override Number Add(Number first, Number second)
            {
                switch (GetCommonClassCode(first, second))
                {
                    case Integer:
                        {
                            int n1 = first.IntValue();
                            int n2 = second.IntValue();
                            int n = n1 + n2;
                            if ((n ^ n1) < 0 && (n ^ n2) < 0)
                            {
                                return new Number.LongNumber(((long) n1) + n2);
                            }
                            return new Number.IntNumber(n);
                        }
                    case Long:
                        {
                            long n1 = first.LongValue();
                            long n2 = second.LongValue();
                            long n = n1 + n2;
                            if ((n ^ n1) < 0 && (n ^ n2) < 0)
                            {
                                throw new OverflowException();
                            }
                            return new Number.LongNumber(n);
                        }
                    case Float:
                        {
                            return new Number.FloatNumber(first.FloatValue() + second.FloatValue());
                        }
                    case Double:
                        {
                            return new Number.DoubleNumber(first.DoubleValue() + second.DoubleValue());
                        }
                    //case BIGINTEGER:
                    //    {
                    //        BigInteger n1 = toBigInteger(first);
                    //        BigInteger n2 = toBigInteger(second);
                    //        return n1.add(n2);
                    //    }
                    //case BIGDECIMAL:
                    //    {
                    //        BigDecimal n1 = toBigDecimal(first);
                    //        BigDecimal n2 = toBigDecimal(second);
                    //        return n1.add(n2);
                    //    }
                }
                // Make the compiler happy. getCommonClassCode() is guaranteed to 
                // return only above codes, or throw an exception.
                throw new InvalidOperationException();
            }

            public override Number Subtract(Number first, Number second)
            {
                switch (GetCommonClassCode(first, second))
                {
                    case Integer:
                        {
                            int n1 = first.IntValue();
                            int n2 = second.IntValue();
                            int n = n1 - n2;
                            if ((n ^ n1) < 0 && (n ^ ~n2) < 0)
                            {
                                return new Number.LongNumber(((long) n1) - n2);
                            }
                            return new Number.IntNumber(n);
                        }
                    case Long:
                        {
                            long n1 = first.LongValue();
                            long n2 = second.LongValue();
                            long n = n1 - n2;
                            if ((n ^ n1) < 0 && (n ^ ~n2) < 0)
                            {
                                throw new OverflowException();
                            }
                            return new Number.LongNumber(n);
                        }
                    case Float:
                        {
                            return new Number.FloatNumber(first.FloatValue() - second.FloatValue());
                        }
                    case Double:
                        {
                            return new Number.DoubleNumber(first.DoubleValue() - second.DoubleValue());
                        }
                    //case BIGINTEGER:
                    //    {
                    //        BigInteger n1 = toBigInteger(first);
                    //        BigInteger n2 = toBigInteger(second);
                    //        return n1.subtract(n2);
                    //    }
                    //case BIGDECIMAL:
                    //    {
                    //        BigDecimal n1 = toBigDecimal(first);
                    //        BigDecimal n2 = toBigDecimal(second);
                    //        return n1.subtract(n2);
                    //    }
                }
                // Make the compiler happy. getCommonClassCode() is guaranteed to 
                // return only above codes, or throw an exception.
                throw new InvalidOperationException();
            }

            public override Number Multiply(Number first, Number second)
            {
                switch (GetCommonClassCode(first, second))
                {
                    case Integer:
                        {
                            int n1 = first.IntValue();
                            int n2 = second.IntValue();
                            int n = n1 * n2;
                            if (n1 == 0 || n/n1 == n2)
                            {
                                return new Number.IntNumber(n);
                            }
                            return new Number.LongNumber(((long) n1)*n2);
                        }
                    case Long:
                        {
                            long n1 = first.LongValue();
                            long n2 = second.LongValue();
                            long n = n1 * n2;
                            if (n1 == 0L || n/n1 == n2)
                            {
                                return new Number.LongNumber(n);
                            }
                            throw new OverflowException();
                        }
                    case Float:
                        {
                            return new Number.FloatNumber(first.FloatValue() * second.FloatValue());
                        }
                    case Double:
                        {
                            return new Number.DoubleNumber(first.DoubleValue() * second.DoubleValue());
                        }
                    //case BIGINTEGER:
                    //    {
                    //        BigInteger n1 = toBigInteger(first);
                    //        BigInteger n2 = toBigInteger(second);
                    //        return n1.multiply(n2);
                    //    }
                    //case BIGDECIMAL:
                    //    {
                    //        BigDecimal n1 = toBigDecimal(first);
                    //        BigDecimal n2 = toBigDecimal(second);
                    //        BigDecimal r = n1.multiply(n2);
                    //        return r.scale() > maxScale ? r.setScale(maxScale, roundingPolicy) : r;
                    //    }
                }
                // Make the compiler happy. getCommonClassCode() is guaranteed to 
                // return only above codes, or throw an exception.
                throw new Exception();
            }

            public override Number Divide(Number first, Number second)
            {
                switch (GetCommonClassCode(first, second))
                {
                    case Integer:
                        {
                            int n1 = first.IntValue();
                            int n2 = second.IntValue();
                            if (n1 % n2 == 0)
                            {
                                return new Number.IntNumber(n1 / n2);
                            }
                            return new Number.DoubleNumber(((double) n1) / n2);
                        }
                    case Long:
                        {
                            long n1 = first.LongValue();
                            long n2 = second.LongValue();
                            if (n1 % n2 == 0)
                            {
                                return new Number.LongNumber(n1 / n2);
                            }
                            return new Number.DoubleNumber(((double) n1) / n2);
                        }
                    case Float:
                        {
                            return new Number.FloatNumber(first.FloatValue() / second.FloatValue());
                        }
                    case Double:
                        {
                            return new Number.DoubleNumber(first.DoubleValue() / second.DoubleValue());
                        }
                    //case BIGINTEGER:
                    //    {
                    //        BigInteger n1 = toBigInteger(first);
                    //        BigInteger n2 = toBigInteger(second);
                    //        BigInteger[] divmod = n1.divideAndRemainder(n2);
                    //        if (divmod[1].equals(BigInteger.ZERO))
                    //        {
                    //            return divmod[0];
                    //        }
                    //        else
                    //        {
                    //            BigDecimal bd1 = new BigDecimal(n1);
                    //            BigDecimal bd2 = new BigDecimal(n2);
                    //            return bd1.divide(bd2, minScale, roundingPolicy);
                    //        }
                    //    }
                    //case BIGDECIMAL:
                    //    {
                    //        BigDecimal n1 = toBigDecimal(first);
                    //        BigDecimal n2 = toBigDecimal(second);
                    //        int scale1 = n1.scale();
                    //        int scale2 = n2.scale();
                    //        int scale = Math.max(scale1, scale2);
                    //        scale = Math.max(minScale, scale);
                    //        return n1.divide(n2, scale, roundingPolicy);
                    //    }
                }
                // Make the compiler happy. getCommonClassCode() is guaranteed to 
                // return only above codes, or throw an exception.
                throw new InvalidOperationException();
            }

            public override Number Modulus(Number first, Number second)
            {
                switch (GetCommonClassCode(first, second))
                {
                    case Integer:
                        {
                            return new Number.IntNumber(first.IntValue() % second.IntValue());
                        }
                    case Long:
                        {
                            return new Number.LongNumber(first.LongValue() % second.LongValue());
                        }
                    case Float:
                        {
                            return new Number.FloatNumber(first.FloatValue() % second.FloatValue());
                        }
                    case Double:
                        {
                            return new Number.DoubleNumber(first.DoubleValue() % second.DoubleValue());
                        }
                    //case BIGINTEGER:
                    //    {
                    //        BigInteger n1 = toBigInteger(first);
                    //        BigInteger n2 = toBigInteger(second);
                    //        return n1.mod(n2);
                    //    }
                    //case BIGDECIMAL:
                    //    {
                    //        throw new TemplateException("Can't calculate remainder on BigDecimals", Environment.getCurrentEnvironment());
                    //    }
                }
                // Make the compiler happy. getCommonClassCode() is guaranteed to 
                // return only above codes, or throw an exception.
                throw new InvalidOperationException();
            }

            public override Number ToNumber(string s)
            {
                // TODO: Was return OptimizerUtil.optimizeNumberRepresentation(new BigDecimal(s));

                short shortValue;
                if (short.TryParse(s, out shortValue))
                {
                    return new Number.ShortNumber(shortValue);
                }
                throw new NotImplementedException();
            }

            private static Dictionary<Type, int> CreateClassCodesMap()
            {
                // TODO: Add remaining number types
                //map.Add(BigInteger.class, new Integer(BIGINTEGER));
                //map.Add(BigDecimal.class, new Integer(BIGDECIMAL));
                return new Dictionary<Type, int>
                {
                    {typeof (Number.ByteNumber), Integer},
                    {typeof (Number.ShortNumber), Integer},
                    {typeof (Number.IntNumber), Integer},
                    {typeof (Number.LongNumber), Long},
                    {typeof (Number.FloatNumber), Float},
                    {typeof (Number.DoubleNumber), Double}
                };
            }

            private static int GetClassCode(Number num)
            {
                if (num == null)
                {
                    throw new TemplateException("Unknown number type null", Environment.GetCurrentEnvironment());
                }

                int classCode;
                if (!ClassCodes.TryGetValue(num.GetType(), out classCode))
                {
                    throw new TemplateException("Unknown number type " + num.GetType().FullName, Environment.GetCurrentEnvironment());
                }
                return classCode;
            }

            private static int GetCommonClassCode(Number num1, Number num2)
            {
                int c1 = GetClassCode(num1);
                int c2 = GetClassCode(num2);
                int c = c1 > c2 ? c1 : c2;
                // If BigInteger is combined with a Float or Double, the result is a
                // BigDecimal instead of BigInteger in order not to lose the 
                // fractional parts. If Float is combined with Long, the result is a
                // Double instead of Float to preserve the bigger bit width.
                switch (c)
                {
                    case Float:
                        {
                            if ((c1 < c2 ? c1 : c2) == Long)
                            {
                                return Double;
                            }
                            break;
                        }
                    // TODO: case BIGINTEGER:
                    //    {
                    //        int min = c1 < c2 ? c1 : c2;
                    //        if (min == Double || min == Float)
                    //        {
                    //            return BIGDECIMAL;
                    //        }
                    //        break;
                    //    }
                }
                return c;
            }
            
            //private static BigInteger toBigInteger(Number num) {
            //    return num is BigInteger ? (BigInteger) num : new BigInteger(num.toString());
            //}
        }

        //private static BigDecimal toBigDecimal(Number num) {
        //    return num is BigDecimal ? (BigDecimal) num : new BigDecimal(num.toString());
        //}
    }
}